/*
 * This file Copyright (C) 2007-2014 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 */

#include <limits.h> /* INT_MAX */
#include <stddef.h>
#include <stdio.h> /* sscanf() */
#include <stdlib.h> /* abort() */
#include <glib/gi18n.h>
#include <gtk/gtk.h>

#include <libtransmission/transmission.h>
#include <libtransmission/utils.h> /* tr_free */

#include "actions.h"
#include "conf.h"
#include "details.h"
#include "favicon.h" /* gtr_get_favicon() */
#include "file-list.h"
#include "hig.h"
#include "tr-prefs.h"
#include "util.h"

static GQuark ARG_KEY = 0;
static GQuark DETAILS_KEY = 0;
static GQuark TORRENT_ID_KEY = 0;
static GQuark TEXT_BUFFER_KEY = 0;
static GQuark URL_ENTRY_KEY = 0;

struct DetailsImpl
{
    GtkWidget* dialog;

    GtkWidget* honor_limits_check;
    GtkWidget* up_limited_check;
    GtkWidget* up_limit_sping;
    GtkWidget* down_limited_check;
    GtkWidget* down_limit_spin;
    GtkWidget* bandwidth_combo;

    GtkWidget* ratio_combo;
    GtkWidget* ratio_spin;
    GtkWidget* idle_combo;
    GtkWidget* idle_spin;
    GtkWidget* max_peers_spin;

    gulong honor_limits_check_tag;
    gulong up_limited_check_tag;
    gulong down_limited_check_tag;
    gulong down_limit_spin_tag;
    gulong up_limit_spin_tag;
    gulong bandwidth_combo_tag;
    gulong ratio_combo_tag;
    gulong ratio_spin_tag;
    gulong idle_combo_tag;
    gulong idle_spin_tag;
    gulong max_peers_spin_tag;

    GtkWidget* size_lb;
    GtkWidget* state_lb;
    GtkWidget* have_lb;
    GtkWidget* dl_lb;
    GtkWidget* ul_lb;
    GtkWidget* error_lb;
    GtkWidget* date_started_lb;
    GtkWidget* eta_lb;
    GtkWidget* last_activity_lb;

    GtkWidget* hash_lb;
    GtkWidget* privacy_lb;
    GtkWidget* origin_lb;
    GtkWidget* destination_lb;
    GtkTextBuffer* comment_buffer;

    GHashTable* peer_hash;
    GHashTable* webseed_hash;
    GtkListStore* peer_store;
    GtkListStore* webseed_store;
    GtkWidget* webseed_view;
    GtkWidget* peer_view;
    GtkWidget* more_peer_details_check;

    GtkListStore* tracker_store;
    GHashTable* tracker_hash;
    GtkTreeModel* trackers_filtered;
    GtkWidget* add_tracker_button;
    GtkWidget* edit_trackers_button;
    GtkWidget* remove_tracker_button;
    GtkWidget* tracker_view;
    GtkWidget* scrape_check;
    GtkWidget* all_check;

    GtkWidget* file_list;
    GtkWidget* file_label;

    GSList* ids;
    TrCore* core;
    guint periodic_refresh_tag;

    GString* gstr;
};

static tr_torrent** getTorrents(struct DetailsImpl* d, int* setmeCount)
{
    int torrentCount = 0;
    int const n = g_slist_length(d->ids);
    tr_torrent** torrents = g_new(tr_torrent*, n);

    for (GSList* l = d->ids; l != NULL; l = l->next)
    {
        if ((torrents[torrentCount] = gtr_core_find_torrent(d->core, GPOINTER_TO_INT(l->data))) != NULL)
        {
            ++torrentCount;
        }
    }

    *setmeCount = torrentCount;
    return torrents;
}

/****
*****
*****  OPTIONS TAB
*****
****/

static void set_togglebutton_if_different(GtkWidget* w, gulong tag, gboolean value)
{
    GtkToggleButton* toggle = GTK_TOGGLE_BUTTON(w);
    gboolean const currentValue = gtk_toggle_button_get_active(toggle);

    if (currentValue != value)
    {
        g_signal_handler_block(toggle, tag);
        gtk_toggle_button_set_active(toggle, value);
        g_signal_handler_unblock(toggle, tag);
    }
}

static void set_int_spin_if_different(GtkWidget* w, gulong tag, int value)
{
    GtkSpinButton* spin = GTK_SPIN_BUTTON(w);
    int const currentValue = gtk_spin_button_get_value_as_int(spin);

    if (currentValue != value)
    {
        g_signal_handler_block(spin, tag);
        gtk_spin_button_set_value(spin, value);
        g_signal_handler_unblock(spin, tag);
    }
}

static void set_double_spin_if_different(GtkWidget* w, gulong tag, double value)
{
    GtkSpinButton* spin = GTK_SPIN_BUTTON(w);
    double const currentValue = gtk_spin_button_get_value(spin);

    if ((int)(currentValue * 100) != (int)(value * 100))
    {
        g_signal_handler_block(spin, tag);
        gtk_spin_button_set_value(spin, value);
        g_signal_handler_unblock(spin, tag);
    }
}

static void unset_combo(GtkWidget* w, gulong tag)
{
    GtkComboBox* combobox = GTK_COMBO_BOX(w);

    g_signal_handler_block(combobox, tag);
    gtk_combo_box_set_active(combobox, -1);
    g_signal_handler_unblock(combobox, tag);
}

static void refreshOptions(struct DetailsImpl* di, tr_torrent** torrents, int n)
{
    /***
    ****  Options Page
    ***/

    /* honor_limits_check */
    if (n != 0)
    {
        bool const baseline = tr_torrentUsesSessionLimits(torrents[0]);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == tr_torrentUsesSessionLimits(torrents[i]);
        }

        if (is_uniform)
        {
            set_togglebutton_if_different(di->honor_limits_check, di->honor_limits_check_tag, baseline);
        }
    }

    /* down_limited_check */
    if (n != 0)
    {
        bool const baseline = tr_torrentUsesSpeedLimit(torrents[0], TR_DOWN);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == tr_torrentUsesSpeedLimit(torrents[i], TR_DOWN);
        }

        if (is_uniform)
        {
            set_togglebutton_if_different(di->down_limited_check, di->down_limited_check_tag, baseline);
        }
    }

    /* down_limit_spin */
    if (n != 0)
    {
        unsigned int const baseline = tr_torrentGetSpeedLimit_KBps(torrents[0], TR_DOWN);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == tr_torrentGetSpeedLimit_KBps(torrents[i], TR_DOWN);
        }

        if (is_uniform)
        {
            set_int_spin_if_different(di->down_limit_spin, di->down_limit_spin_tag, baseline);
        }
    }

    /* up_limited_check */
    if (n != 0)
    {
        bool const baseline = tr_torrentUsesSpeedLimit(torrents[0], TR_UP);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == tr_torrentUsesSpeedLimit(torrents[i], TR_UP);
        }

        if (is_uniform)
        {
            set_togglebutton_if_different(di->up_limited_check, di->up_limited_check_tag, baseline);
        }
    }

    /* up_limit_sping */
    if (n != 0)
    {
        unsigned int const baseline = tr_torrentGetSpeedLimit_KBps(torrents[0], TR_UP);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == tr_torrentGetSpeedLimit_KBps(torrents[i], TR_UP);
        }

        if (is_uniform)
        {
            set_int_spin_if_different(di->up_limit_sping, di->up_limit_spin_tag, baseline);
        }
    }

    /* bandwidth_combo */
    if (n != 0)
    {
        int const baseline = tr_torrentGetPriority(torrents[0]);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == tr_torrentGetPriority(torrents[i]);
        }

        if (is_uniform)
        {
            GtkWidget* w = di->bandwidth_combo;
            g_signal_handler_block(w, di->bandwidth_combo_tag);
            gtr_priority_combo_set_value(GTK_COMBO_BOX(w), baseline);
            g_signal_handler_unblock(w, di->bandwidth_combo_tag);
        }
        else
        {
            unset_combo(di->bandwidth_combo, di->bandwidth_combo_tag);
        }
    }

    /* ratio_combo */
    if (n != 0)
    {
        int const baseline = tr_torrentGetRatioMode(torrents[0]);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == (int)tr_torrentGetRatioMode(torrents[i]);
        }

        if (is_uniform)
        {
            GtkWidget* w = di->ratio_combo;
            g_signal_handler_block(w, di->ratio_combo_tag);
            gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), baseline);
            gtr_widget_set_visible(di->ratio_spin, baseline == TR_RATIOLIMIT_SINGLE);
            g_signal_handler_unblock(w, di->ratio_combo_tag);
        }
    }

    /* ratio_spin */
    if (n != 0)
    {
        double const baseline = tr_torrentGetRatioLimit(torrents[0]);
        set_double_spin_if_different(di->ratio_spin, di->ratio_spin_tag, baseline);
    }

    /* idle_combo */
    if (n != 0)
    {
        int const baseline = tr_torrentGetIdleMode(torrents[0]);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == (int)tr_torrentGetIdleMode(torrents[i]);
        }

        if (is_uniform)
        {
            GtkWidget* w = di->idle_combo;
            g_signal_handler_block(w, di->idle_combo_tag);
            gtr_combo_box_set_active_enum(GTK_COMBO_BOX(w), baseline);
            gtr_widget_set_visible(di->idle_spin, baseline == TR_IDLELIMIT_SINGLE);
            g_signal_handler_unblock(w, di->idle_combo_tag);
        }
    }

    /* idle_spin */
    if (n != 0)
    {
        int const baseline = tr_torrentGetIdleLimit(torrents[0]);
        set_int_spin_if_different(di->idle_spin, di->idle_spin_tag, baseline);
    }

    /* max_peers_spin */
    if (n != 0)
    {
        int const baseline = tr_torrentGetPeerLimit(torrents[0]);
        set_int_spin_if_different(di->max_peers_spin, di->max_peers_spin_tag, baseline);
    }
}

static void torrent_set_bool(struct DetailsImpl* di, tr_quark const key, gboolean value)
{
    tr_variant top;
    tr_variant* args;
    tr_variant* ids;

    tr_variantInitDict(&top, 2);
    tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
    args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
    tr_variantDictAddBool(args, key, value);
    ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids));

    for (GSList* l = di->ids; l != NULL; l = l->next)
    {
        tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data));
    }

    gtr_core_exec(di->core, &top);
    tr_variantFree(&top);
}

static void torrent_set_int(struct DetailsImpl* di, tr_quark const key, int value)
{
    tr_variant top;
    tr_variant* args;
    tr_variant* ids;

    tr_variantInitDict(&top, 2);
    tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
    args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
    tr_variantDictAddInt(args, key, value);
    ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids));

    for (GSList* l = di->ids; l != NULL; l = l->next)
    {
        tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data));
    }

    gtr_core_exec(di->core, &top);
    tr_variantFree(&top);
}

static void torrent_set_real(struct DetailsImpl* di, tr_quark const key, double value)
{
    tr_variant top;
    tr_variant* args;
    tr_variant* ids;

    tr_variantInitDict(&top, 2);
    tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
    args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
    tr_variantDictAddReal(args, key, value);
    ids = tr_variantDictAddList(args, TR_KEY_ids, g_slist_length(di->ids));

    for (GSList* l = di->ids; l != NULL; l = l->next)
    {
        tr_variantListAddInt(ids, GPOINTER_TO_INT(l->data));
    }

    gtr_core_exec(di->core, &top);
    tr_variantFree(&top);
}

static void up_speed_toggled_cb(GtkToggleButton* tb, gpointer d)
{
    torrent_set_bool(d, TR_KEY_uploadLimited, gtk_toggle_button_get_active(tb));
}

static void down_speed_toggled_cb(GtkToggleButton* tb, gpointer d)
{
    torrent_set_bool(d, TR_KEY_downloadLimited, gtk_toggle_button_get_active(tb));
}

static void global_speed_toggled_cb(GtkToggleButton* tb, gpointer d)
{
    torrent_set_bool(d, TR_KEY_honorsSessionLimits, gtk_toggle_button_get_active(tb));
}

static void up_speed_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
{
    torrent_set_int(di, TR_KEY_uploadLimit, gtk_spin_button_get_value_as_int(s));
}

static void down_speed_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
{
    torrent_set_int(di, TR_KEY_downloadLimit, gtk_spin_button_get_value_as_int(s));
}

static void idle_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
{
    torrent_set_int(di, TR_KEY_seedIdleLimit, gtk_spin_button_get_value_as_int(s));
}

static void ratio_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
{
    torrent_set_real(di, TR_KEY_seedRatioLimit, gtk_spin_button_get_value(s));
}

static void max_peers_spun_cb(GtkSpinButton* s, struct DetailsImpl* di)
{
    torrent_set_int(di, TR_KEY_peer_limit, gtk_spin_button_get_value(s));
}

static void onPriorityChanged(GtkComboBox* combo_box, struct DetailsImpl* di)
{
    tr_priority_t const priority = gtr_priority_combo_get_value(combo_box);
    torrent_set_int(di, TR_KEY_bandwidthPriority, priority);
}

static GtkWidget* new_priority_combo(struct DetailsImpl* di)
{
    GtkWidget* w = gtr_priority_combo_new();
    di->bandwidth_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onPriorityChanged), di);
    return w;
}

static void refresh(struct DetailsImpl* di);

static void onComboEnumChanged(GtkComboBox* combo_box, struct DetailsImpl* di)
{
    tr_quark const key = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(combo_box), ARG_KEY));
    torrent_set_int(di, key, gtr_combo_box_get_active_enum(combo_box));
    refresh(di);
}

static GtkWidget* ratio_combo_new(void)
{
    GtkWidget* w = gtr_combo_box_new_enum(
        _("Use global settings"), TR_RATIOLIMIT_GLOBAL,
        _("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED,
        _("Stop seeding at ratio:"), TR_RATIOLIMIT_SINGLE,
        NULL);
    g_object_set_qdata(G_OBJECT(w), ARG_KEY, GINT_TO_POINTER(TR_KEY_seedRatioMode));
    return w;
}

static GtkWidget* idle_combo_new(void)
{
    GtkWidget* w = gtr_combo_box_new_enum(
        _("Use global settings"), TR_IDLELIMIT_GLOBAL,
        _("Seed regardless of activity"), TR_IDLELIMIT_UNLIMITED,
        _("Stop seeding if idle for N minutes:"), TR_IDLELIMIT_SINGLE,
        NULL);
    g_object_set_qdata(G_OBJECT(w), ARG_KEY, GINT_TO_POINTER(TR_KEY_seedIdleMode));
    return w;
}

static GtkWidget* options_page_new(struct DetailsImpl* d)
{
    guint row;
    gulong tag;
    char buf[128];
    GtkWidget* t;
    GtkWidget* w;
    GtkWidget* tb;
    GtkWidget* h;

    row = 0;
    t = hig_workarea_create();
    hig_workarea_add_section_title(t, &row, _("Speed"));

    tb = hig_workarea_add_wide_checkbutton(t, &row, _("Honor global _limits"), 0);
    d->honor_limits_check = tb;
    tag = g_signal_connect(tb, "toggled", G_CALLBACK(global_speed_toggled_cb), d);
    d->honor_limits_check_tag = tag;

    g_snprintf(buf, sizeof(buf), _("Limit _download speed (%s):"), _(speed_K_str));
    tb = gtk_check_button_new_with_mnemonic(buf);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(tb), FALSE);
    d->down_limited_check = tb;
    tag = g_signal_connect(tb, "toggled", G_CALLBACK(down_speed_toggled_cb), d);
    d->down_limited_check_tag = tag;

    w = gtk_spin_button_new_with_range(0, INT_MAX, 5);
    tag = g_signal_connect(w, "value-changed", G_CALLBACK(down_speed_spun_cb), d);
    d->down_limit_spin_tag = tag;
    hig_workarea_add_row_w(t, &row, tb, w, NULL);
    d->down_limit_spin = w;

    g_snprintf(buf, sizeof(buf), _("Limit _upload speed (%s):"), _(speed_K_str));
    tb = gtk_check_button_new_with_mnemonic(buf);
    d->up_limited_check = tb;
    tag = g_signal_connect(tb, "toggled", G_CALLBACK(up_speed_toggled_cb), d);
    d->up_limited_check_tag = tag;

    w = gtk_spin_button_new_with_range(0, INT_MAX, 5);
    tag = g_signal_connect(w, "value-changed", G_CALLBACK(up_speed_spun_cb), d);
    d->up_limit_spin_tag = tag;
    hig_workarea_add_row_w(t, &row, tb, w, NULL);
    d->up_limit_sping = w;

    w = new_priority_combo(d);
    hig_workarea_add_row(t, &row, _("Torrent _priority:"), w, NULL);
    d->bandwidth_combo = w;

    hig_workarea_add_section_divider(t, &row);
    hig_workarea_add_section_title(t, &row, _("Seeding Limits"));

    h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD);
    w = d->ratio_combo = ratio_combo_new();
    d->ratio_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onComboEnumChanged), d);
    gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
    w = d->ratio_spin = gtk_spin_button_new_with_range(0, 1000, .05);
    gtk_entry_set_width_chars(GTK_ENTRY(w), 7);
    d->ratio_spin_tag = g_signal_connect(w, "value-changed", G_CALLBACK(ratio_spun_cb), d);
    gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0);
    hig_workarea_add_row(t, &row, _("_Ratio:"), h, NULL);

    h = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD);
    w = d->idle_combo = idle_combo_new();
    d->idle_combo_tag = g_signal_connect(w, "changed", G_CALLBACK(onComboEnumChanged), d);
    gtk_box_pack_start(GTK_BOX(h), w, TRUE, TRUE, 0);
    w = d->idle_spin = gtk_spin_button_new_with_range(1, 40320, 5);
    d->idle_spin_tag = g_signal_connect(w, "value-changed", G_CALLBACK(idle_spun_cb), d);
    gtk_box_pack_start(GTK_BOX(h), w, FALSE, FALSE, 0);
    hig_workarea_add_row(t, &row, _("_Idle:"), h, NULL);

    hig_workarea_add_section_divider(t, &row);
    hig_workarea_add_section_title(t, &row, _("Peer Connections"));

    w = gtk_spin_button_new_with_range(1, 3000, 5);
    hig_workarea_add_row(t, &row, _("_Maximum peers:"), w, w);
    tag = g_signal_connect(w, "value-changed", G_CALLBACK(max_peers_spun_cb), d);
    d->max_peers_spin = w;
    d->max_peers_spin_tag = tag;

    return t;
}

/****
*****
*****  INFO TAB
*****
****/

static char const* activityString(int activity, bool finished)
{
    switch (activity)
    {
    case TR_STATUS_CHECK_WAIT:
        return _("Queued for verification");

    case TR_STATUS_CHECK:
        return _("Verifying local data");

    case TR_STATUS_DOWNLOAD_WAIT:
        return _("Queued for download");

    case TR_STATUS_DOWNLOAD:
        return C_("Verb", "Downloading");

    case TR_STATUS_SEED_WAIT:
        return _("Queued for seeding");

    case TR_STATUS_SEED:
        return C_("Verb", "Seeding");

    case TR_STATUS_STOPPED:
        return finished ? _("Finished") : _("Paused");
    }

    return "";
}

/* Only call gtk_text_buffer_set_text () if the new text differs from the old.
 * This way if the user has text selected, refreshing won't deselect it */
static void gtr_text_buffer_set_text(GtkTextBuffer* b, char const* str)
{
    char* old_str;
    GtkTextIter start;
    GtkTextIter end;

    if (str == NULL)
    {
        str = "";
    }

    gtk_text_buffer_get_bounds(b, &start, &end);
    old_str = gtk_text_buffer_get_text(b, &start, &end, FALSE);

    if (old_str == NULL || g_strcmp0(old_str, str) != 0)
    {
        gtk_text_buffer_set_text(b, str, -1);
    }

    g_free(old_str);
}

static char* get_short_date_string(time_t t)
{
    char buf[64];
    struct tm tm;

    if (t == 0)
    {
        return g_strdup(_("N/A"));
    }

    tr_localtime_r(&t, &tm);
    strftime(buf, sizeof(buf), "%d %b %Y", &tm);
    return g_locale_to_utf8(buf, -1, NULL, NULL, NULL);
};

static void refreshInfo(struct DetailsImpl* di, tr_torrent** torrents, int n)
{
    char const* str;
    char const* mixed = _("Mixed");
    char const* no_torrent = _("No Torrents Selected");
    char const* stateString;
    char buf[512];
    uint64_t sizeWhenDone = 0;
    tr_stat const** stats = g_new(tr_stat const*, n);
    tr_info const** infos = g_new(tr_info const*, n);

    for (int i = 0; i < n; ++i)
    {
        stats[i] = tr_torrentStatCached(torrents[i]);
        infos[i] = tr_torrentInfo(torrents[i]);
    }

    /* privacy_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        bool const baseline = infos[0]->isPrivate;
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == infos[i]->isPrivate;
        }

        if (is_uniform)
        {
            str = baseline ? _("Private to this tracker -- DHT and PEX disabled") : _("Public torrent");
        }
        else
        {
            str = mixed;
        }
    }

    gtr_label_set_text(GTK_LABEL(di->privacy_lb), str);

    /* origin_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        char const* creator = infos[0]->creator != NULL ? infos[0]->creator : "";
        time_t const date = infos[0]->dateCreated;
        char* datestr = get_short_date_string(date);
        gboolean mixed_creator = FALSE;
        gboolean mixed_date = FALSE;

        for (int i = 1; i < n; ++i)
        {
            mixed_creator |= g_strcmp0(creator, infos[i]->creator != NULL ? infos[i]->creator : "") != 0;
            mixed_date |= date != infos[i]->dateCreated;
        }

        gboolean const empty_creator = tr_str_is_empty(creator);
        gboolean const empty_date = date == 0;

        if (mixed_date || mixed_creator)
        {
            str = mixed;
        }
        else if (empty_date && empty_creator)
        {
            str = _("N/A");
        }
        else
        {
            if (empty_date && !empty_creator)
            {
                g_snprintf(buf, sizeof(buf), _("Created by %1$s"), creator);
            }
            else if (empty_creator && !empty_date)
            {
                g_snprintf(buf, sizeof(buf), _("Created on %1$s"), datestr);
            }
            else
            {
                g_snprintf(buf, sizeof(buf), _("Created by %1$s on %2$s"), creator, datestr);
            }

            str = buf;
        }

        g_free(datestr);
    }

    gtr_label_set_text(GTK_LABEL(di->origin_lb), str);

    /* comment_buffer */
    if (n <= 0)
    {
        str = "";
    }
    else
    {
        char const* baseline = infos[0]->comment != NULL ? infos[0]->comment : "";
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = g_strcmp0(baseline, infos[i]->comment != NULL ? infos[i]->comment : "") == 0;
        }

        str = is_uniform ? baseline : mixed;
    }

    gtr_text_buffer_set_text(di->comment_buffer, str);

    /* destination_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        char const* baseline = tr_torrentGetDownloadDir(torrents[0]);
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = g_strcmp0(baseline, tr_torrentGetDownloadDir(torrents[i])) == 0;
        }

        str = is_uniform ? baseline : mixed;
    }

    gtr_label_set_text(GTK_LABEL(di->destination_lb), str);

    /* state_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        tr_torrent_activity const activity = stats[0]->activity;
        bool is_uniform = true;
        bool allFinished = stats[0]->finished;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = activity == stats[i]->activity;

            if (!stats[i]->finished)
            {
                allFinished = false;
            }
        }

        str = is_uniform ? activityString(activity, allFinished) : mixed;
    }

    stateString = str;
    gtr_label_set_text(GTK_LABEL(di->state_lb), str);

    /* date started */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        time_t const baseline = stats[0]->startDate;
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == stats[i]->startDate;
        }

        if (!is_uniform)
        {
            str = mixed;
        }
        else if (baseline <= 0 || stats[0]->activity == TR_STATUS_STOPPED)
        {
            str = stateString;
        }
        else
        {
            str = tr_strltime(buf, time(NULL) - baseline, sizeof(buf));
        }
    }

    gtr_label_set_text(GTK_LABEL(di->date_started_lb), str);

    /* eta */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        int const baseline = stats[0]->eta;
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = baseline == stats[i]->eta;
        }

        if (!is_uniform)
        {
            str = mixed;
        }
        else if (baseline < 0)
        {
            str = _("Unknown");
        }
        else
        {
            str = tr_strltime(buf, baseline, sizeof(buf));
        }
    }

    gtr_label_set_text(GTK_LABEL(di->eta_lb), str);

    /* size_lb */
    {
        char sizebuf[128];
        uint64_t size = 0;
        int pieces = 0;
        int32_t pieceSize = 0;

        for (int i = 0; i < n; ++i)
        {
            size += infos[i]->totalSize;
            pieces += infos[i]->pieceCount;

            if (pieceSize == 0)
            {
                pieceSize = infos[i]->pieceSize;
            }
            else if (pieceSize != (int)infos[i]->pieceSize)
            {
                pieceSize = -1;
            }
        }

        tr_strlsize(sizebuf, size, sizeof(sizebuf));

        if (size == 0)
        {
            str = "";
        }
        else if (pieceSize >= 0)
        {
            char piecebuf[128];
            tr_formatter_mem_B(piecebuf, pieceSize, sizeof(piecebuf));
            g_snprintf(buf, sizeof(buf), ngettext("%1$s (%2$'d piece @ %3$s)", "%1$s (%2$'d pieces @ %3$s)", pieces), sizebuf,
                pieces, piecebuf);
            str = buf;
        }
        else
        {
            g_snprintf(buf, sizeof(buf), ngettext("%1$s (%2$'d piece)", "%1$s (%2$'d pieces)", pieces), sizebuf, pieces);
            str = buf;
        }

        gtr_label_set_text(GTK_LABEL(di->size_lb), str);
    }

    /* have_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        uint64_t leftUntilDone = 0;
        uint64_t haveUnchecked = 0;
        uint64_t haveValid = 0;
        uint64_t available = 0;

        for (int i = 0; i < n; ++i)
        {
            tr_stat const* st = stats[i];
            haveUnchecked += st->haveUnchecked;
            haveValid += st->haveValid;
            sizeWhenDone += st->sizeWhenDone;
            leftUntilDone += st->leftUntilDone;
            available += st->sizeWhenDone - st->leftUntilDone + st->desiredAvailable;
        }

        {
            char buf2[32];
            char unver[64];
            char total[64];
            char avail[32];
            double const d = sizeWhenDone != 0 ? (100.0 * available) / sizeWhenDone : 0;
            double const ratio = 100.0 * (sizeWhenDone != 0 ? (haveValid + haveUnchecked) / (double)sizeWhenDone : 1);

            tr_strlpercent(avail, d, sizeof(avail));
            tr_strlpercent(buf2, ratio, sizeof(buf2));
            tr_strlsize(total, haveUnchecked + haveValid, sizeof(total));
            tr_strlsize(unver, haveUnchecked, sizeof(unver));

            if (haveUnchecked == 0 && leftUntilDone == 0)
            {
                g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%%)"), total, buf2);
            }
            else if (haveUnchecked == 0)
            {
                g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%% of %3$s%% Available)"), total, buf2, avail);
            }
            else
            {
                g_snprintf(buf, sizeof(buf), _("%1$s (%2$s%% of %3$s%% Available); %4$s Unverified"), total, buf2, avail,
                    unver);
            }

            str = buf;
        }
    }

    gtr_label_set_text(GTK_LABEL(di->have_lb), str);

    /* dl_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        char dbuf[64];
        char fbuf[64];
        uint64_t d = 0;
        uint64_t f = 0;

        for (int i = 0; i < n; ++i)
        {
            d += stats[i]->downloadedEver;
            f += stats[i]->corruptEver;
        }

        tr_strlsize(dbuf, d, sizeof(dbuf));
        tr_strlsize(fbuf, f, sizeof(fbuf));

        if (f != 0)
        {
            g_snprintf(buf, sizeof(buf), _("%1$s (+%2$s corrupt)"), dbuf, fbuf);
        }
        else
        {
            tr_strlcpy(buf, dbuf, sizeof(buf));
        }

        str = buf;
    }

    gtr_label_set_text(GTK_LABEL(di->dl_lb), str);

    /* ul_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        char upstr[64];
        char ratiostr[64];
        uint64_t up = 0;
        uint64_t down = 0;

        for (int i = 0; i < n; ++i)
        {
            up += stats[i]->uploadedEver;
            down += stats[i]->downloadedEver;
        }

        tr_strlsize(upstr, up, sizeof(upstr));
        tr_strlratio(ratiostr, tr_getRatio(up, down), sizeof(ratiostr));
        g_snprintf(buf, sizeof(buf), _("%s (Ratio: %s)"), upstr, ratiostr);
        str = buf;
    }

    gtr_label_set_text(GTK_LABEL(di->ul_lb), str);

    /* hash_lb */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else if (n == 1)
    {
        str = infos[0]->hashString;
    }
    else
    {
        str = mixed;
    }

    gtr_label_set_text(GTK_LABEL(di->hash_lb), str);

    /* error */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        char const* baseline = stats[0]->errorString;
        bool is_uniform = true;

        for (int i = 1; is_uniform && i < n; ++i)
        {
            is_uniform = g_strcmp0(baseline, stats[i]->errorString) == 0;
        }

        str = is_uniform ? baseline : mixed;
    }

    if (tr_str_is_empty(str))
    {
        str = _("No errors");
    }

    gtr_label_set_text(GTK_LABEL(di->error_lb), str);

    /* activity date */
    if (n <= 0)
    {
        str = no_torrent;
    }
    else
    {
        time_t latest = 0;

        for (int i = 0; i < n; ++i)
        {
            if (latest < stats[i]->activityDate)
            {
                latest = stats[i]->activityDate;
            }
        }

        if (latest <= 0)
        {
            str = _("Never");
        }
        else
        {
            int const period = time(NULL) - latest;

            if (period < 5)
            {
                tr_strlcpy(buf, _("Active now"), sizeof(buf));
            }
            else
            {
                char tbuf[128];
                tr_strltime(tbuf, period, sizeof(tbuf));
                g_snprintf(buf, sizeof(buf), _("%1$s ago"), tbuf);
            }

            str = buf;
        }
    }

    gtr_label_set_text(GTK_LABEL(di->last_activity_lb), str);

    g_free(stats);
    g_free(infos);
}

static GtkWidget* info_page_new(struct DetailsImpl* di)
{
    guint row = 0;
    GtkTextBuffer* b;
    GtkWidget* l;
    GtkWidget* w;
    GtkWidget* fr;
    GtkWidget* sw;
    GtkWidget* t = hig_workarea_create();

    hig_workarea_add_section_title(t, &row, _("Activity"));

    /* size */
    l = di->size_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Torrent size:"), l, NULL);

    /* have */
    l = di->have_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Have:"), l, NULL);

    /* uploaded */
    l = di->ul_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Uploaded:"), l, NULL);

    /* downloaded */
    l = di->dl_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Downloaded:"), l, NULL);

    /* state */
    l = di->state_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("State:"), l, NULL);

    /* running for */
    l = di->date_started_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Running time:"), l, NULL);

    /* eta */
    l = di->eta_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Remaining time:"), l, NULL);

    /* last activity */
    l = di->last_activity_lb = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Last activity:"), l, NULL);

    /* error */
    l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    hig_workarea_add_row(t, &row, _("Error:"), l, NULL);
    di->error_lb = l;

    hig_workarea_add_section_divider(t, &row);
    hig_workarea_add_section_title(t, &row, _("Details"));

    /* destination */
    l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    hig_workarea_add_row(t, &row, _("Location:"), l, NULL);
    di->destination_lb = l;

    /* hash */
    l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    hig_workarea_add_row(t, &row, _("Hash:"), l, NULL);
    di->hash_lb = l;

    /* privacy */
    l = gtk_label_new(NULL);
    gtk_label_set_single_line_mode(GTK_LABEL(l), TRUE);
    hig_workarea_add_row(t, &row, _("Privacy:"), l, NULL);
    di->privacy_lb = l;

    /* origins */
    l = g_object_new(GTK_TYPE_LABEL, "selectable", TRUE, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    hig_workarea_add_row(t, &row, _("Origin:"), l, NULL);
    di->origin_lb = l;

    /* comment */
    b = di->comment_buffer = gtk_text_buffer_new(NULL);
    w = gtk_text_view_new_with_buffer(b);
    gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(w), GTK_WRAP_WORD);
    gtk_text_view_set_editable(GTK_TEXT_VIEW(w), FALSE);
    sw = gtk_scrolled_window_new(NULL, NULL);
    gtk_widget_set_size_request(sw, 350, 36);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(sw), w);
    fr = gtk_frame_new(NULL);
    gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_IN);
    gtk_container_add(GTK_CONTAINER(fr), sw);
    w = hig_workarea_add_tall_row(t, &row, _("Comment:"), fr, NULL);
    g_object_set(w, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_START, NULL);

    hig_workarea_add_section_divider(t, &row);
    return t;
}

/****
*****
*****  PEERS TAB
*****
****/

enum
{
    WEBSEED_COL_KEY,
    WEBSEED_COL_WAS_UPDATED,
    WEBSEED_COL_URL,
    WEBSEED_COL_DOWNLOAD_RATE_DOUBLE,
    WEBSEED_COL_DOWNLOAD_RATE_STRING,
    N_WEBSEED_COLS
};

static char const* getWebseedColumnNames(int column)
{
    switch (column)
    {
    case WEBSEED_COL_URL:
        return _("Web Seeds");

    case WEBSEED_COL_DOWNLOAD_RATE_DOUBLE:
    case WEBSEED_COL_DOWNLOAD_RATE_STRING:
        return _("Down");

    default:
        return "";
    }
}

static GtkListStore* webseed_model_new(void)
{
    return gtk_list_store_new(N_WEBSEED_COLS,
        G_TYPE_STRING, /* key */
        G_TYPE_BOOLEAN, /* was-updated */
        G_TYPE_STRING, /* url */
        G_TYPE_DOUBLE, /* download rate double */
        G_TYPE_STRING); /* download rate string */
}

enum
{
    PEER_COL_KEY,
    PEER_COL_WAS_UPDATED,
    PEER_COL_ADDRESS,
    PEER_COL_ADDRESS_COLLATED,
    PEER_COL_DOWNLOAD_RATE_DOUBLE,
    PEER_COL_DOWNLOAD_RATE_STRING,
    PEER_COL_UPLOAD_RATE_DOUBLE,
    PEER_COL_UPLOAD_RATE_STRING,
    PEER_COL_CLIENT,
    PEER_COL_PROGRESS,
    PEER_COL_UPLOAD_REQUEST_COUNT_INT,
    PEER_COL_UPLOAD_REQUEST_COUNT_STRING,
    PEER_COL_DOWNLOAD_REQUEST_COUNT_INT,
    PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING,
    PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT,
    PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING,
    PEER_COL_BLOCKS_UPLOADED_COUNT_INT,
    PEER_COL_BLOCKS_UPLOADED_COUNT_STRING,
    PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT,
    PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING,
    PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT,
    PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING,
    PEER_COL_ENCRYPTION_STOCK_ID,
    PEER_COL_FLAGS,
    PEER_COL_TORRENT_NAME,
    N_PEER_COLS
};

static char const* getPeerColumnName(int column)
{
    switch (column)
    {
    case PEER_COL_ADDRESS:
        return _("Address");

    case PEER_COL_DOWNLOAD_RATE_STRING:
    case PEER_COL_DOWNLOAD_RATE_DOUBLE:
        return _("Down");

    case PEER_COL_UPLOAD_RATE_STRING:
    case PEER_COL_UPLOAD_RATE_DOUBLE:
        return _("Up");

    case PEER_COL_CLIENT:
        return _("Client");

    case PEER_COL_PROGRESS:
        return _("%");

    case PEER_COL_UPLOAD_REQUEST_COUNT_INT:
    case PEER_COL_UPLOAD_REQUEST_COUNT_STRING:
        return _("Up Reqs");

    case PEER_COL_DOWNLOAD_REQUEST_COUNT_INT:
    case PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING:
        return _("Dn Reqs");

    case PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT:
    case PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING:
        return _("Dn Blocks");

    case PEER_COL_BLOCKS_UPLOADED_COUNT_INT:
    case PEER_COL_BLOCKS_UPLOADED_COUNT_STRING:
        return _("Up Blocks");

    case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT:
    case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING:
        return _("We Cancelled");

    case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT:
    case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING:
        return _("They Cancelled");

    case PEER_COL_FLAGS:
        return _("Flags");

    default:
        return "";
    }
}

static GtkListStore* peer_store_new(void)
{
    return gtk_list_store_new(N_PEER_COLS,
        G_TYPE_STRING, /* key */
        G_TYPE_BOOLEAN, /* was-updated */
        G_TYPE_STRING, /* address */
        G_TYPE_STRING, /* collated address */
        G_TYPE_DOUBLE, /* download speed int */
        G_TYPE_STRING, /* download speed string */
        G_TYPE_DOUBLE, /* upload speed int */
        G_TYPE_STRING, /* upload speed string */
        G_TYPE_STRING, /* client */
        G_TYPE_INT, /* progress [0..100] */
        G_TYPE_INT, /* upload request count int */
        G_TYPE_STRING, /* upload request count string */
        G_TYPE_INT, /* download request count int */
        G_TYPE_STRING, /* download request count string */
        G_TYPE_INT, /* # blocks downloaded int */
        G_TYPE_STRING, /* # blocks downloaded string */
        G_TYPE_INT, /* # blocks uploaded int */
        G_TYPE_STRING, /* # blocks uploaded string */
        G_TYPE_INT, /* # blocks cancelled by client int */
        G_TYPE_STRING, /* # blocks cancelled by client string */
        G_TYPE_INT, /* # blocks cancelled by peer int */
        G_TYPE_STRING, /* # blocks cancelled by peer string */
        G_TYPE_STRING, /* encryption stock id */
        G_TYPE_STRING, /* flagString */
        G_TYPE_STRING); /* torrent name */
}

static void initPeerRow(GtkListStore* store, GtkTreeIter* iter, char const* key, char const* torrentName,
    tr_peer_stat const* peer)
{
    int q[4];
    char collated_name[128];
    char const* client = peer->client;

    if (client == NULL || g_strcmp0(client, "Unknown Client") == 0)
    {
        client = "";
    }

    if (sscanf(peer->addr, "%d.%d.%d.%d", q, q + 1, q + 2, q + 3) != 4)
    {
        g_strlcpy(collated_name, peer->addr, sizeof(collated_name));
    }
    else
    {
        g_snprintf(collated_name, sizeof(collated_name), "%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3]);
    }

    gtk_list_store_set(store, iter,
        PEER_COL_ADDRESS, peer->addr,
        PEER_COL_ADDRESS_COLLATED, collated_name,
        PEER_COL_CLIENT, client,
        PEER_COL_ENCRYPTION_STOCK_ID, peer->isEncrypted ? "transmission-lock" : NULL,
        PEER_COL_KEY, key,
        PEER_COL_TORRENT_NAME, torrentName,
        -1);
}

static void refreshPeerRow(GtkListStore* store, GtkTreeIter* iter, tr_peer_stat const* peer)
{
    char up_speed[64] = { '\0' };
    char down_speed[64] = { '\0' };
    char up_count[64] = { '\0' };
    char down_count[64] = { '\0' };
    char blocks_to_peer[64] = { '\0' };
    char blocks_to_client[64] = { '\0' };
    char cancelled_by_peer[64] = { '\0' };
    char cancelled_by_client[64] = { '\0' };

    if (peer->rateToPeer_KBps > 0.01)
    {
        tr_formatter_speed_KBps(up_speed, peer->rateToPeer_KBps, sizeof(up_speed));
    }

    if (peer->rateToClient_KBps > 0)
    {
        tr_formatter_speed_KBps(down_speed, peer->rateToClient_KBps, sizeof(down_speed));
    }

    if (peer->pendingReqsToPeer > 0)
    {
        g_snprintf(down_count, sizeof(down_count), "%d", peer->pendingReqsToPeer);
    }

    if (peer->pendingReqsToClient > 0)
    {
        g_snprintf(up_count, sizeof(down_count), "%d", peer->pendingReqsToClient);
    }

    if (peer->blocksToPeer > 0)
    {
        g_snprintf(blocks_to_peer, sizeof(blocks_to_peer), "%" PRIu32, peer->blocksToPeer);
    }

    if (peer->blocksToClient > 0)
    {
        g_snprintf(blocks_to_client, sizeof(blocks_to_client), "%" PRIu32, peer->blocksToClient);
    }

    if (peer->cancelsToPeer > 0)
    {
        g_snprintf(cancelled_by_client, sizeof(cancelled_by_client), "%" PRIu32, peer->cancelsToPeer);
    }

    if (peer->cancelsToClient > 0)
    {
        g_snprintf(cancelled_by_peer, sizeof(cancelled_by_peer), "%" PRIu32, peer->cancelsToClient);
    }

    gtk_list_store_set(store, iter,
        PEER_COL_PROGRESS, (int)(100.0 * peer->progress),
        PEER_COL_UPLOAD_REQUEST_COUNT_INT, peer->pendingReqsToClient,
        PEER_COL_UPLOAD_REQUEST_COUNT_STRING, up_count,
        PEER_COL_DOWNLOAD_REQUEST_COUNT_INT, peer->pendingReqsToPeer,
        PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING, down_count,
        PEER_COL_DOWNLOAD_RATE_DOUBLE, peer->rateToClient_KBps,
        PEER_COL_DOWNLOAD_RATE_STRING, down_speed,
        PEER_COL_UPLOAD_RATE_DOUBLE, peer->rateToPeer_KBps,
        PEER_COL_UPLOAD_RATE_STRING, up_speed,
        PEER_COL_FLAGS, peer->flagStr,
        PEER_COL_WAS_UPDATED, TRUE,
        PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT, (int)peer->blocksToClient,
        PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING, blocks_to_client,
        PEER_COL_BLOCKS_UPLOADED_COUNT_INT, (int)peer->blocksToPeer,
        PEER_COL_BLOCKS_UPLOADED_COUNT_STRING, blocks_to_peer,
        PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT, (int)peer->cancelsToPeer,
        PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING, cancelled_by_client,
        PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT, (int)peer->cancelsToClient,
        PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING, cancelled_by_peer,
        -1);
}

static void refreshPeerList(struct DetailsImpl* di, tr_torrent** torrents, int n)
{
    int* peerCount;
    GtkTreeIter iter;
    GtkTreeModel* model;
    GHashTable* hash = di->peer_hash;
    GtkListStore* store = di->peer_store;
    struct tr_peer_stat** peers;

    /* step 1: get all the peers */
    peers = g_new(struct tr_peer_stat*, n);
    peerCount = g_new(int, n);

    for (int i = 0; i < n; ++i)
    {
        peers[i] = tr_torrentPeers(torrents[i], &peerCount[i]);
    }

    /* step 2: mark all the peers in the list as not-updated */
    model = GTK_TREE_MODEL(store);

    if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
        do
        {
            gtk_list_store_set(store, &iter, PEER_COL_WAS_UPDATED, FALSE, -1);
        }
        while (gtk_tree_model_iter_next(model, &iter));
    }

    /* step 3: add any new peers */
    for (int i = 0; i < n; ++i)
    {
        tr_torrent const* tor = torrents[i];

        for (int j = 0; j < peerCount[i]; ++j)
        {
            char key[128];
            tr_peer_stat const* s = &peers[i][j];

            g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr);

            if (g_hash_table_lookup(hash, key) == NULL)
            {
                GtkTreePath* p;
                gtk_list_store_append(store, &iter);
                initPeerRow(store, &iter, key, tr_torrentName(tor), s);
                p = gtk_tree_model_get_path(model, &iter);
                g_hash_table_insert(hash, g_strdup(key), gtk_tree_row_reference_new(model, p));
                gtk_tree_path_free(p);
            }
        }
    }

    /* step 4: update the peers */
    for (int i = 0; i < n; ++i)
    {
        tr_torrent const* tor = torrents[i];

        for (int j = 0; j < peerCount[i]; ++j)
        {
            char key[128];
            GtkTreePath* p;
            GtkTreeRowReference* ref;
            tr_peer_stat const* s = &peers[i][j];

            g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), s->addr);
            ref = g_hash_table_lookup(hash, key);
            p = gtk_tree_row_reference_get_path(ref);
            gtk_tree_model_get_iter(model, &iter, p);
            refreshPeerRow(store, &iter, s);
            gtk_tree_path_free(p);
        }
    }

    /* step 5: remove peers that have disappeared */
    model = GTK_TREE_MODEL(store);

    if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
        gboolean more = TRUE;

        while (more)
        {
            gboolean b;
            gtk_tree_model_get(model, &iter, PEER_COL_WAS_UPDATED, &b, -1);

            if (b)
            {
                more = gtk_tree_model_iter_next(model, &iter);
            }
            else
            {
                char* key;
                gtk_tree_model_get(model, &iter, PEER_COL_KEY, &key, -1);
                g_hash_table_remove(hash, key);
                more = gtk_list_store_remove(store, &iter);
                g_free(key);
            }
        }
    }

    /* step 6: cleanup */
    for (int i = 0; i < n; ++i)
    {
        tr_torrentPeersFree(peers[i], peerCount[i]);
    }

    tr_free(peers);
    tr_free(peerCount);
}

static void refreshWebseedList(struct DetailsImpl* di, tr_torrent** torrents, int n)
{
    int total = 0;
    GtkTreeIter iter;
    GHashTable* hash = di->webseed_hash;
    GtkListStore* store = di->webseed_store;
    GtkTreeModel* model = GTK_TREE_MODEL(store);

    /* step 1: mark all webseeds as not-updated */
    if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
        do
        {
            gtk_list_store_set(store, &iter, WEBSEED_COL_WAS_UPDATED, FALSE, -1);
        }
        while (gtk_tree_model_iter_next(model, &iter));
    }

    /* step 2: add any new webseeds */
    for (int i = 0; i < n; ++i)
    {
        tr_torrent const* tor = torrents[i];
        tr_info const* inf = tr_torrentInfo(tor);

        total += inf->webseedCount;

        for (unsigned int j = 0; j < inf->webseedCount; ++j)
        {
            char key[256];
            char const* url = inf->webseeds[j];
            g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), url);

            if (g_hash_table_lookup(hash, key) == NULL)
            {
                GtkTreePath* p;
                gtk_list_store_append(store, &iter);
                gtk_list_store_set(store, &iter,
                    WEBSEED_COL_URL, url,
                    WEBSEED_COL_KEY, key,
                    -1);
                p = gtk_tree_model_get_path(model, &iter);
                g_hash_table_insert(hash, g_strdup(key), gtk_tree_row_reference_new(model, p));
                gtk_tree_path_free(p);
            }
        }
    }

    /* step 3: update the webseeds */
    for (int i = 0; i < n; ++i)
    {
        tr_torrent* tor = torrents[i];
        tr_info const* inf = tr_torrentInfo(tor);
        double* speeds_KBps = tr_torrentWebSpeeds_KBps(tor);

        for (unsigned int j = 0; j < inf->webseedCount; ++j)
        {
            char buf[128];
            char key[256];
            GtkTreePath* p;
            GtkTreeRowReference* ref;
            char const* url = inf->webseeds[j];

            g_snprintf(key, sizeof(key), "%d.%s", tr_torrentId(tor), url);
            ref = g_hash_table_lookup(hash, key);
            p = gtk_tree_row_reference_get_path(ref);
            gtk_tree_model_get_iter(model, &iter, p);

            if (speeds_KBps[j] > 0)
            {
                tr_formatter_speed_KBps(buf, speeds_KBps[j], sizeof(buf));
            }
            else
            {
                *buf = '\0';
            }

            gtk_list_store_set(store, &iter,
                WEBSEED_COL_DOWNLOAD_RATE_DOUBLE, speeds_KBps[j],
                WEBSEED_COL_DOWNLOAD_RATE_STRING, buf,
                WEBSEED_COL_WAS_UPDATED, TRUE,
                -1);

            gtk_tree_path_free(p);
        }

        tr_free(speeds_KBps);
    }

    /* step 4: remove webseeds that have disappeared */
    if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
        gboolean more = TRUE;

        while (more)
        {
            gboolean b;
            gtk_tree_model_get(model, &iter, WEBSEED_COL_WAS_UPDATED, &b, -1);

            if (b)
            {
                more = gtk_tree_model_iter_next(model, &iter);
            }
            else
            {
                char* key;
                gtk_tree_model_get(model, &iter, WEBSEED_COL_KEY, &key, -1);

                if (key != NULL)
                {
                    g_hash_table_remove(hash, key);
                }

                more = gtk_list_store_remove(store, &iter);
                g_free(key);
            }
        }
    }

    /* most of the time there are no webseeds...
       don't waste space showing an empty list */
    gtk_widget_set_visible(di->webseed_view, total > 0);
}

static void refreshPeers(struct DetailsImpl* di, tr_torrent** torrents, int n)
{
    refreshPeerList(di, torrents, n);
    refreshWebseedList(di, torrents, n);
}

static gboolean onPeerViewQueryTooltip(GtkWidget* widget, gint x, gint y, gboolean keyboard_tip, GtkTooltip* tooltip,
    gpointer gdi)
{
    GtkTreeIter iter;
    GtkTreeModel* model;
    gboolean show_tip = FALSE;

    if (gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y, keyboard_tip, &model, NULL, &iter))
    {
        char* name = NULL;
        char* addr = NULL;
        char* markup = NULL;
        char* flagstr = NULL;
        struct DetailsImpl* di = gdi;
        GString* gstr = di->gstr;

        gtk_tree_model_get(model, &iter,
            PEER_COL_TORRENT_NAME, &name,
            PEER_COL_ADDRESS, &addr,
            PEER_COL_FLAGS, &flagstr,
            -1);

        g_string_truncate(gstr, 0);
        markup = g_markup_escape_text(name, -1);
        g_string_append_printf(gstr, "<b>%s</b>\n%s\n \n", markup, addr);
        g_free(markup);

        for (char const* pch = flagstr; !tr_str_is_empty(pch); ++pch)
        {
            char const* s = NULL;

            switch (*pch)
            {
            case 'O':
                s = _("Optimistic unchoke");
                break;

            case 'D':
                s = _("Downloading from this peer");
                break;

            case 'd':
                s = _("We would download from this peer if they would let us");
                break;

            case 'U':
                s = _("Uploading to peer");
                break;

            case 'u':
                s = _("We would upload to this peer if they asked");
                break;

            case 'K':
                s = _("Peer has unchoked us, but we're not interested");
                break;

            case '?':
                s = _("We unchoked this peer, but they're not interested");
                break;

            case 'E':
                s = _("Encrypted connection");
                break;

            case 'X':
                s = _("Peer was found through Peer Exchange (PEX)");
                break;

            case 'H':
                s = _("Peer was found through DHT");
                break;

            case 'I':
                s = _("Peer is an incoming connection");
                break;

            case 'T':
                s = _("Peer is connected over µTP");
                break;
            }

            if (s != NULL)
            {
                g_string_append_printf(gstr, "%c: %s\n", *pch, s);
            }
        }

        if (gstr->len != 0) /* remove the last linefeed */
        {
            g_string_set_size(gstr, gstr->len - 1);
        }

        gtk_tooltip_set_markup(tooltip, gstr->str);

        g_free(flagstr);
        g_free(addr);
        g_free(name);
        show_tip = TRUE;
    }

    return show_tip;
}

static void setPeerViewColumns(GtkTreeView* peer_view)
{
    int n;
    int view_columns[32];
    GtkCellRenderer* r;
    GtkTreeViewColumn* c;
    bool const more = gtr_pref_flag_get(TR_KEY_show_extra_peer_details);

    n = 0;
    view_columns[n++] = PEER_COL_ENCRYPTION_STOCK_ID;
    view_columns[n++] = PEER_COL_UPLOAD_RATE_STRING;

    if (more)
    {
        view_columns[n++] = PEER_COL_UPLOAD_REQUEST_COUNT_STRING;
    }

    view_columns[n++] = PEER_COL_DOWNLOAD_RATE_STRING;

    if (more)
    {
        view_columns[n++] = PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING;
    }

    if (more)
    {
        view_columns[n++] = PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING;
    }

    if (more)
    {
        view_columns[n++] = PEER_COL_BLOCKS_UPLOADED_COUNT_STRING;
    }

    if (more)
    {
        view_columns[n++] = PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING;
    }

    if (more)
    {
        view_columns[n++] = PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING;
    }

    view_columns[n++] = PEER_COL_PROGRESS;
    view_columns[n++] = PEER_COL_FLAGS;
    view_columns[n++] = PEER_COL_ADDRESS;
    view_columns[n++] = PEER_COL_CLIENT;

    /* remove any existing columns */
    while ((c = gtk_tree_view_get_column(peer_view, 0)) != NULL)
    {
        gtk_tree_view_remove_column(peer_view, c);
    }

    for (int i = 0; i < n; ++i)
    {
        int const col = view_columns[i];
        char const* t = getPeerColumnName(col);
        int sort_col = col;

        switch (col)
        {
        case PEER_COL_ADDRESS:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_ADDRESS_COLLATED;
            break;

        case PEER_COL_CLIENT:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            break;

        case PEER_COL_PROGRESS:
            r = gtk_cell_renderer_progress_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "value", PEER_COL_PROGRESS, NULL);
            break;

        case PEER_COL_ENCRYPTION_STOCK_ID:
            r = gtk_cell_renderer_pixbuf_new();
            g_object_set(r, "xalign", (gfloat)0.0, "yalign", (gfloat)0.5, NULL);
            c = gtk_tree_view_column_new_with_attributes(t, r, "stock-id", PEER_COL_ENCRYPTION_STOCK_ID, NULL);
            gtk_tree_view_column_set_sizing(c, GTK_TREE_VIEW_COLUMN_FIXED);
            gtk_tree_view_column_set_fixed_width(c, 20);
            break;

        case PEER_COL_DOWNLOAD_REQUEST_COUNT_STRING:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_DOWNLOAD_REQUEST_COUNT_INT;
            break;

        case PEER_COL_UPLOAD_REQUEST_COUNT_STRING:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_UPLOAD_REQUEST_COUNT_INT;
            break;

        case PEER_COL_BLOCKS_DOWNLOADED_COUNT_STRING:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_BLOCKS_DOWNLOADED_COUNT_INT;
            break;

        case PEER_COL_BLOCKS_UPLOADED_COUNT_STRING:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_BLOCKS_UPLOADED_COUNT_INT;
            break;

        case PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_STRING:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_REQS_CANCELLED_BY_CLIENT_COUNT_INT;
            break;

        case PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_STRING:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_REQS_CANCELLED_BY_PEER_COUNT_INT;
            break;

        case PEER_COL_DOWNLOAD_RATE_STRING:
            r = gtk_cell_renderer_text_new();
            g_object_set(G_OBJECT(r), "xalign", 1.0F, NULL);
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_DOWNLOAD_RATE_DOUBLE;
            break;

        case PEER_COL_UPLOAD_RATE_STRING:
            r = gtk_cell_renderer_text_new();
            g_object_set(G_OBJECT(r), "xalign", 1.0F, NULL);
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            sort_col = PEER_COL_UPLOAD_RATE_DOUBLE;
            break;

        case PEER_COL_FLAGS:
            r = gtk_cell_renderer_text_new();
            c = gtk_tree_view_column_new_with_attributes(t, r, "text", col, NULL);
            break;

        default:
            abort();
        }

        gtk_tree_view_column_set_resizable(c, FALSE);
        gtk_tree_view_column_set_sort_column_id(c, sort_col);
        gtk_tree_view_append_column(GTK_TREE_VIEW(peer_view), c);
    }

    /* the 'expander' column has a 10-pixel margin on the left
       that doesn't look quite correct in any of these columns...
       so create a non-visible column and assign it as the
       'expander column. */
    {
        GtkTreeViewColumn* c = gtk_tree_view_column_new();
        gtk_tree_view_column_set_visible(c, FALSE);
        gtk_tree_view_append_column(GTK_TREE_VIEW(peer_view), c);
        gtk_tree_view_set_expander_column(GTK_TREE_VIEW(peer_view), c);
    }
}

static void onMorePeerInfoToggled(GtkToggleButton* button, struct DetailsImpl* di)
{
    tr_quark const key = TR_KEY_show_extra_peer_details;
    gboolean const value = gtk_toggle_button_get_active(button);
    gtr_core_set_pref_bool(di->core, key, value);
    setPeerViewColumns(GTK_TREE_VIEW(di->peer_view));
}

static GtkWidget* peer_page_new(struct DetailsImpl* di)
{
    gboolean b;
    char const* str;
    GtkListStore* store;
    GtkWidget* v;
    GtkWidget* w;
    GtkWidget* ret;
    GtkWidget* sw;
    GtkWidget* vbox;
    GtkWidget* webtree = NULL;
    GtkTreeModel* m;
    GtkTreeViewColumn* c;
    GtkCellRenderer* r;

    /* webseeds */

    store = di->webseed_store = webseed_model_new();
    v = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
    g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);
    g_object_unref(store);

    str = getWebseedColumnNames(WEBSEED_COL_URL);
    r = gtk_cell_renderer_text_new();
    g_object_set(G_OBJECT(r), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    c = gtk_tree_view_column_new_with_attributes(str, r, "text", WEBSEED_COL_URL, NULL);
    g_object_set(G_OBJECT(c), "expand", TRUE, NULL);
    gtk_tree_view_column_set_sort_column_id(c, WEBSEED_COL_URL);
    gtk_tree_view_append_column(GTK_TREE_VIEW(v), c);

    str = getWebseedColumnNames(WEBSEED_COL_DOWNLOAD_RATE_STRING);
    r = gtk_cell_renderer_text_new();
    c = gtk_tree_view_column_new_with_attributes(str, r, "text", WEBSEED_COL_DOWNLOAD_RATE_STRING, NULL);
    gtk_tree_view_column_set_sort_column_id(c, WEBSEED_COL_DOWNLOAD_RATE_DOUBLE);
    gtk_tree_view_append_column(GTK_TREE_VIEW(v), c);

    w = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN);
    gtk_container_add(GTK_CONTAINER(w), v);

    webtree = w;
    di->webseed_view = w;

    /* peers */

    store = di->peer_store = peer_store_new();
    m = gtk_tree_model_sort_new_with_model(GTK_TREE_MODEL(store));
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(m), PEER_COL_PROGRESS, GTK_SORT_DESCENDING);
    v = GTK_WIDGET(g_object_new(GTK_TYPE_TREE_VIEW, "model", m, "rules-hint", TRUE, "has-tooltip", TRUE, NULL));
    di->peer_view = v;

    g_signal_connect(v, "query-tooltip", G_CALLBACK(onPeerViewQueryTooltip), di);
    g_object_unref(store);
    g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);

    setPeerViewColumns(GTK_TREE_VIEW(v));

    w = sw = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(w), GTK_SHADOW_IN);
    gtk_container_add(GTK_CONTAINER(w), v);

    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), GUI_PAD_BIG);

    v = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
    gtk_paned_pack1(GTK_PANED(v), webtree, FALSE, TRUE);
    gtk_paned_pack2(GTK_PANED(v), sw, TRUE, TRUE);
    gtk_box_pack_start(GTK_BOX(vbox), v, TRUE, TRUE, 0);

    w = gtk_check_button_new_with_mnemonic(_("Show _more details"));
    di->more_peer_details_check = w;
    b = gtr_pref_flag_get(TR_KEY_show_extra_peer_details);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b);
    g_signal_connect(w, "toggled", G_CALLBACK(onMorePeerInfoToggled), di);
    gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0);

    /* ip-to-GtkTreeRowReference */
    di->peer_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free,
        (GDestroyNotify)gtk_tree_row_reference_free);

    /* url-to-GtkTreeRowReference */
    di->webseed_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free,
        (GDestroyNotify)gtk_tree_row_reference_free);
    ret = vbox;
    return ret;
}

/****
*****
*****  TRACKER
*****
****/

/* if it's been longer than a minute, don't bother showing the seconds */
static void tr_strltime_rounded(char* buf, time_t t, size_t buflen)
{
    if (t > 60)
    {
        t -= (t % 60);
    }

    tr_strltime(buf, t, buflen);
}

static void buildTrackerSummary(GString* gstr, char const* key, tr_tracker_stat const* st, gboolean showScrape)
{
    char* str;
    char timebuf[256];
    time_t const now = time(NULL);
    char const* err_markup_begin = "<span color=\"red\">";
    char const* err_markup_end = "</span>";
    char const* timeout_markup_begin = "<span color=\"#224466\">";
    char const* timeout_markup_end = "</span>";
    char const* success_markup_begin = "<span color=\"#008B00\">";
    char const* success_markup_end = "</span>";

    /* hostname */
    {
        g_string_append(gstr, st->isBackup ? "<i>" : "<b>");

        if (key != NULL)
        {
            str = g_markup_printf_escaped("%s - %s", st->host, key);
        }
        else
        {
            str = g_markup_printf_escaped("%s", st->host);
        }

        g_string_append(gstr, str);
        g_free(str);
        g_string_append(gstr, st->isBackup ? "</i>" : "</b>");
    }

    if (!st->isBackup)
    {
        if (st->hasAnnounced && st->announceState != TR_TRACKER_INACTIVE)
        {
            g_string_append_c(gstr, '\n');
            tr_strltime_rounded(timebuf, now - st->lastAnnounceTime, sizeof(timebuf));

            if (st->lastAnnounceSucceeded)
            {
                g_string_append_printf(gstr, _("Got a list of %1$s%2$'d peers%3$s %4$s ago"),
                    success_markup_begin, st->lastAnnouncePeerCount, success_markup_end, timebuf);
            }
            else if (st->lastAnnounceTimedOut)
            {
                g_string_append_printf(gstr, _("Peer list request %1$stimed out%2$s %3$s ago; will retry"),
                    timeout_markup_begin, timeout_markup_end, timebuf);
            }
            else
            {
                g_string_append_printf(gstr, _("Got an error %1$s\"%2$s\"%3$s %4$s ago"), err_markup_begin,
                    st->lastAnnounceResult, err_markup_end, timebuf);
            }
        }

        switch (st->announceState)
        {
        case TR_TRACKER_INACTIVE:
            g_string_append_c(gstr, '\n');
            g_string_append(gstr, _("No updates scheduled"));
            break;

        case TR_TRACKER_WAITING:
            tr_strltime_rounded(timebuf, st->nextAnnounceTime - now, sizeof(timebuf));
            g_string_append_c(gstr, '\n');
            g_string_append_printf(gstr, _("Asking for more peers in %s"), timebuf);
            break;

        case TR_TRACKER_QUEUED:
            g_string_append_c(gstr, '\n');
            g_string_append(gstr, _("Queued to ask for more peers"));
            break;

        case TR_TRACKER_ACTIVE:
            tr_strltime_rounded(timebuf, now - st->lastAnnounceStartTime, sizeof(timebuf));
            g_string_append_c(gstr, '\n');
            g_string_append_printf(gstr, _("Asking for more peers now… <small>%s</small>"), timebuf);
            break;
        }

        if (showScrape)
        {
            if (st->hasScraped)
            {
                g_string_append_c(gstr, '\n');
                tr_strltime_rounded(timebuf, now - st->lastScrapeTime, sizeof(timebuf));

                if (st->lastScrapeSucceeded)
                {
                    g_string_append_printf(gstr, _("Tracker had %s%'d seeders and %'d leechers%s %s ago"), success_markup_begin,
                        st->seederCount, st->leecherCount, success_markup_end, timebuf);
                }
                else
                {
                    g_string_append_printf(gstr, _("Got a scrape error \"%s%s%s\" %s ago"), err_markup_begin,
                        st->lastScrapeResult, err_markup_end, timebuf);
                }
            }

            switch (st->scrapeState)
            {
            case TR_TRACKER_INACTIVE:
                break;

            case TR_TRACKER_WAITING:
                g_string_append_c(gstr, '\n');
                tr_strltime_rounded(timebuf, st->nextScrapeTime - now, sizeof(timebuf));
                g_string_append_printf(gstr, _("Asking for peer counts in %s"), timebuf);
                break;

            case TR_TRACKER_QUEUED:
                g_string_append_c(gstr, '\n');
                g_string_append(gstr, _("Queued to ask for peer counts"));
                break;

            case TR_TRACKER_ACTIVE:
                g_string_append_c(gstr, '\n');
                tr_strltime_rounded(timebuf, now - st->lastScrapeStartTime, sizeof(timebuf));
                g_string_append_printf(gstr, _("Asking for peer counts now… <small>%s</small>"), timebuf);
                break;
            }
        }
    }
}

enum
{
    TRACKER_COL_TORRENT_ID,
    TRACKER_COL_TEXT,
    TRACKER_COL_IS_BACKUP,
    TRACKER_COL_TRACKER_ID,
    TRACKER_COL_FAVICON,
    TRACKER_COL_WAS_UPDATED,
    TRACKER_COL_KEY,
    TRACKER_N_COLS
};

static gboolean trackerVisibleFunc(GtkTreeModel* model, GtkTreeIter* iter, gpointer data)
{
    gboolean isBackup;
    struct DetailsImpl* di = data;

    /* show all */
    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(di->all_check)))
    {
        return TRUE;
    }

    /* don't show the backups... */
    gtk_tree_model_get(model, iter, TRACKER_COL_IS_BACKUP, &isBackup, -1);
    return !isBackup;
}

static int tracker_list_get_current_torrent_id(struct DetailsImpl* di)
{
    int torrent_id = -1;

    /* if there's only one torrent in the dialog, always use it */
    if (torrent_id < 0)
    {
        if (g_slist_length(di->ids) == 1)
        {
            torrent_id = GPOINTER_TO_INT(di->ids->data);
        }
    }

    /* otherwise, use the selected tracker's torrent */
    if (torrent_id < 0)
    {
        GtkTreeIter iter;
        GtkTreeModel* model;
        GtkTreeSelection* sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(di->tracker_view));

        if (gtk_tree_selection_get_selected(sel, &model, &iter))
        {
            gtk_tree_model_get(model, &iter, TRACKER_COL_TORRENT_ID, &torrent_id, -1);
        }
    }

    return torrent_id;
}

static tr_torrent* tracker_list_get_current_torrent(struct DetailsImpl* di)
{
    int const torrent_id = tracker_list_get_current_torrent_id(di);
    return gtr_core_find_torrent(di->core, torrent_id);
}

static void favicon_ready_cb(gpointer pixbuf, gpointer vreference)
{
    GtkTreeIter iter;
    GtkTreeRowReference* reference = vreference;

    if (pixbuf != NULL)
    {
        GtkTreePath* path = gtk_tree_row_reference_get_path(reference);
        GtkTreeModel* model = gtk_tree_row_reference_get_model(reference);

        if (gtk_tree_model_get_iter(model, &iter, path))
        {
            gtk_list_store_set(GTK_LIST_STORE(model), &iter, TRACKER_COL_FAVICON, pixbuf, -1);
        }

        gtk_tree_path_free(path);
        g_object_unref(pixbuf);
    }

    gtk_tree_row_reference_free(reference);
}

static void refreshTracker(struct DetailsImpl* di, tr_torrent** torrents, int n)
{
    int* statCount;
    tr_tracker_stat** stats;
    GtkTreeIter iter;
    GtkTreeModel* model;
    GString* gstr = di->gstr; /* buffer for temporary strings */
    GHashTable* hash = di->tracker_hash;
    GtkListStore* store = di->tracker_store;
    tr_session* session = gtr_core_session(di->core);
    gboolean const showScrape = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(di->scrape_check));

    /* step 1: get all the trackers */
    statCount = g_new0(int, n);
    stats = g_new0(tr_tracker_stat*, n);

    for (int i = 0; i < n; ++i)
    {
        stats[i] = tr_torrentTrackers(torrents[i], &statCount[i]);
    }

    /* step 2: mark all the trackers in the list as not-updated */
    model = GTK_TREE_MODEL(store);

    if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
        do
        {
            gtk_list_store_set(store, &iter, TRACKER_COL_WAS_UPDATED, FALSE, -1);
        }
        while (gtk_tree_model_iter_next(model, &iter));
    }

    /* step 3: add any new trackers */
    for (int i = 0; i < n; ++i)
    {
        int const jn = statCount[i];

        for (int j = 0; j < jn; ++j)
        {
            tr_torrent const* tor = torrents[i];
            tr_tracker_stat const* st = &stats[i][j];
            int const torrent_id = tr_torrentId(tor);

            /* build the key to find the row */
            g_string_truncate(gstr, 0);
            g_string_append_printf(gstr, "%d\t%d\t%s", torrent_id, st->tier, st->announce);

            if (g_hash_table_lookup(hash, gstr->str) == NULL)
            {
                GtkTreePath* p;
                GtkTreeIter iter;
                GtkTreeRowReference* ref;

                gtk_list_store_insert_with_values(store, &iter, -1,
                    TRACKER_COL_TORRENT_ID, torrent_id,
                    TRACKER_COL_TRACKER_ID, st->id,
                    TRACKER_COL_KEY, gstr->str,
                    -1);

                p = gtk_tree_model_get_path(model, &iter);
                ref = gtk_tree_row_reference_new(model, p);
                g_hash_table_insert(hash, g_strdup(gstr->str), ref);
                ref = gtk_tree_row_reference_new(model, p);
                gtr_get_favicon_from_url(session, st->announce, favicon_ready_cb, ref);
                gtk_tree_path_free(p);
            }
        }
    }

    /* step 4: update the peers */
    for (int i = 0; i < n; ++i)
    {
        tr_torrent const* tor = torrents[i];
        char const* summary_name = n > 1 ? tr_torrentName(tor) : NULL;

        for (int j = 0; j < statCount[i]; ++j)
        {
            GtkTreePath* p;
            GtkTreeRowReference* ref;
            tr_tracker_stat const* st = &stats[i][j];

            /* build the key to find the row */
            g_string_truncate(gstr, 0);
            g_string_append_printf(gstr, "%d\t%d\t%s", tr_torrentId(tor), st->tier, st->announce);
            ref = g_hash_table_lookup(hash, gstr->str);
            p = gtk_tree_row_reference_get_path(ref);
            gtk_tree_model_get_iter(model, &iter, p);

            /* update the row */
            g_string_truncate(gstr, 0);
            buildTrackerSummary(gstr, summary_name, st, showScrape);
            gtk_list_store_set(store, &iter,
                TRACKER_COL_TEXT, gstr->str,
                TRACKER_COL_IS_BACKUP, st->isBackup,
                TRACKER_COL_TRACKER_ID, st->id,
                TRACKER_COL_WAS_UPDATED, TRUE,
                -1);

            /* cleanup */
            gtk_tree_path_free(p);
        }
    }

    /* step 5: remove trackers that have disappeared */
    if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
        gboolean more = TRUE;

        while (more)
        {
            gboolean b;
            gtk_tree_model_get(model, &iter, TRACKER_COL_WAS_UPDATED, &b, -1);

            if (b)
            {
                more = gtk_tree_model_iter_next(model, &iter);
            }
            else
            {
                char* key;
                gtk_tree_model_get(model, &iter, TRACKER_COL_KEY, &key, -1);
                g_hash_table_remove(hash, key);
                more = gtk_list_store_remove(store, &iter);
                g_free(key);
            }
        }
    }

    gtk_widget_set_sensitive(di->edit_trackers_button, tracker_list_get_current_torrent_id(di) >= 0);

    /* cleanup */
    for (int i = 0; i < n; ++i)
    {
        tr_torrentTrackersFree(stats[i], statCount[i]);
    }

    g_free(stats);
    g_free(statCount);
}

static void onScrapeToggled(GtkToggleButton* button, struct DetailsImpl* di)
{
    tr_quark const key = TR_KEY_show_tracker_scrapes;
    gboolean const value = gtk_toggle_button_get_active(button);
    gtr_core_set_pref_bool(di->core, key, value);
    refresh(di);
}

static void onBackupToggled(GtkToggleButton* button, struct DetailsImpl* di)
{
    tr_quark const key = TR_KEY_show_backup_trackers;
    gboolean const value = gtk_toggle_button_get_active(button);
    gtr_core_set_pref_bool(di->core, key, value);
    refresh(di);
}

static void on_edit_trackers_response(GtkDialog* dialog, int response, gpointer data)
{
    gboolean do_destroy = TRUE;
    struct DetailsImpl* di = data;

    if (response == GTK_RESPONSE_ACCEPT)
    {
        int n;
        int tier;
        GtkTextIter start;
        GtkTextIter end;
        int const torrent_id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(dialog), TORRENT_ID_KEY));
        GtkTextBuffer* text_buffer = g_object_get_qdata(G_OBJECT(dialog), TEXT_BUFFER_KEY);
        tr_torrent* tor = gtr_core_find_torrent(di->core, torrent_id);

        if (tor != NULL)
        {
            tr_tracker_info* trackers;
            char** tracker_strings;
            char* tracker_text;

            /* build the array of trackers */
            gtk_text_buffer_get_bounds(text_buffer, &start, &end);
            tracker_text = gtk_text_buffer_get_text(text_buffer, &start, &end, FALSE);
            tracker_strings = g_strsplit(tracker_text, "\n", 0);

            trackers = g_new0(tr_tracker_info, g_strv_length(tracker_strings));
            n = 0;
            tier = 0;

            for (int i = 0; tracker_strings[i] != NULL; ++i)
            {
                char* const str = tracker_strings[i];

                if (tr_str_is_empty(str))
                {
                    ++tier;
                }
                else
                {
                    trackers[n].tier = tier;
                    trackers[n].announce = str;
                    ++n;
                }
            }

            /* update the torrent */
            if (tr_torrentSetAnnounceList(tor, trackers, n))
            {
                refresh(di);
            }
            else
            {
                GtkWidget* w;
                char const* text = _("List contains invalid URLs");
                w = gtk_message_dialog_new(GTK_WINDOW(dialog), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s",
                    text);
                gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(w), "%s",
                    _("Please correct the errors and try again."));
                gtk_dialog_run(GTK_DIALOG(w));
                gtk_widget_destroy(w);
                do_destroy = FALSE;
            }

            /* cleanup */
            g_free(trackers);
            g_strfreev(tracker_strings);
            g_free(tracker_text);
        }
    }

    if (do_destroy)
    {
        gtk_widget_destroy(GTK_WIDGET(dialog));
    }
}

static void get_editable_tracker_list(GString* gstr, tr_torrent const* tor)
{
    int tier = 0;
    tr_info const* inf = tr_torrentInfo(tor);

    for (unsigned int i = 0; i < inf->trackerCount; ++i)
    {
        tr_tracker_info const* t = &inf->trackers[i];

        if (tier != t->tier)
        {
            tier = t->tier;
            g_string_append_c(gstr, '\n');
        }

        g_string_append_printf(gstr, "%s\n", t->announce);
    }

    if (gstr->len > 0)
    {
        g_string_truncate(gstr, gstr->len - 1);
    }
}

static void on_edit_trackers(GtkButton* button, gpointer data)
{
    struct DetailsImpl* di = data;
    tr_torrent* tor = tracker_list_get_current_torrent(di);

    if (tor != NULL)
    {
        guint row;
        GtkWidget* w;
        GtkWidget* d;
        GtkWidget* fr;
        GtkWidget* t;
        GtkWidget* l;
        GtkWidget* sw;
        GtkWindow* win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
        GString* gstr = di->gstr; /* buffer for temporary strings */
        int const torrent_id = tr_torrentId(tor);

        g_string_truncate(gstr, 0);
        g_string_append_printf(gstr, _("%s - Edit Trackers"), tr_torrentName(tor));
        d = gtk_dialog_new_with_buttons(gstr->str, win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL,
            GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL);
        g_signal_connect(d, "response", G_CALLBACK(on_edit_trackers_response), data);

        row = 0;
        t = hig_workarea_create();
        hig_workarea_add_section_title(t, &row, _("Tracker Announce URLs"));

        l = gtk_label_new(NULL);
        gtk_label_set_markup(GTK_LABEL(l), _("To add a backup URL, add it on the line after the primary URL.\n"
            "To add another primary URL, add it after a blank line."));
        gtk_label_set_justify(GTK_LABEL(l), GTK_JUSTIFY_LEFT);
        g_object_set(l, "halign", GTK_ALIGN_START, "valign", GTK_ALIGN_CENTER, NULL);
        hig_workarea_add_wide_control(t, &row, l);

        w = gtk_text_view_new();
        g_string_truncate(gstr, 0);
        get_editable_tracker_list(gstr, tor);
        gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(w)), gstr->str, -1);
        fr = gtk_frame_new(NULL);
        gtk_frame_set_shadow_type(GTK_FRAME(fr), GTK_SHADOW_IN);
        sw = gtk_scrolled_window_new(NULL, NULL);
        gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
        gtk_container_add(GTK_CONTAINER(sw), w);
        gtk_container_add(GTK_CONTAINER(fr), sw);
        gtk_widget_set_size_request(fr, 500U, 166U);
        hig_workarea_add_wide_tall_control(t, &row, fr);

        gtr_dialog_set_content(GTK_DIALOG(d), t);

        g_object_set_qdata(G_OBJECT(d), TORRENT_ID_KEY, GINT_TO_POINTER(torrent_id));
        g_object_set_qdata(G_OBJECT(d), TEXT_BUFFER_KEY, gtk_text_view_get_buffer(GTK_TEXT_VIEW(w)));
        gtk_widget_show(d);
    }
}

static void on_tracker_list_selection_changed(GtkTreeSelection* sel, gpointer gdi)
{
    struct DetailsImpl* di = gdi;
    int const n = gtk_tree_selection_count_selected_rows(sel);
    tr_torrent* tor = tracker_list_get_current_torrent(di);

    gtk_widget_set_sensitive(di->remove_tracker_button, n > 0);
    gtk_widget_set_sensitive(di->add_tracker_button, tor != NULL);
    gtk_widget_set_sensitive(di->edit_trackers_button, tor != NULL);
}

static void on_add_tracker_response(GtkDialog* dialog, int response, gpointer gdi)
{
    gboolean destroy = TRUE;

    if (response == GTK_RESPONSE_ACCEPT)
    {
        struct DetailsImpl* di = gdi;
        GtkWidget* e = GTK_WIDGET(g_object_get_qdata(G_OBJECT(dialog), URL_ENTRY_KEY));
        int const torrent_id = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(dialog), TORRENT_ID_KEY));
        char* url = g_strdup(gtk_entry_get_text(GTK_ENTRY(e)));
        g_strstrip(url);

        if (!tr_str_is_empty(url))
        {
            if (tr_urlIsValidTracker(url))
            {
                tr_variant top;
                tr_variant* args;
                tr_variant* trackers;

                tr_variantInitDict(&top, 2);
                tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
                args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
                tr_variantDictAddInt(args, TR_KEY_id, torrent_id);
                trackers = tr_variantDictAddList(args, TR_KEY_trackerAdd, 1);
                tr_variantListAddStr(trackers, url);

                gtr_core_exec(di->core, &top);
                refresh(di);

                tr_variantFree(&top);
            }
            else
            {
                gtr_unrecognized_url_dialog(GTK_WIDGET(dialog), url);
                destroy = FALSE;
            }
        }

        g_free(url);
    }

    if (destroy)
    {
        gtk_widget_destroy(GTK_WIDGET(dialog));
    }
}

static void on_tracker_list_add_button_clicked(GtkButton* button UNUSED, gpointer gdi)
{
    struct DetailsImpl* di = gdi;
    tr_torrent* tor = tracker_list_get_current_torrent(di);

    if (tor != NULL)
    {
        guint row;
        GtkWidget* e;
        GtkWidget* t;
        GtkWidget* w;
        GString* gstr = di->gstr; /* buffer for temporary strings */

        g_string_truncate(gstr, 0);
        g_string_append_printf(gstr, _("%s - Add Tracker"), tr_torrentName(tor));
        w = gtk_dialog_new_with_buttons(gstr->str, GTK_WINDOW(di->dialog), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL,
            GTK_RESPONSE_CANCEL, GTK_STOCK_ADD, GTK_RESPONSE_ACCEPT, NULL);
        g_signal_connect(w, "response", G_CALLBACK(on_add_tracker_response), gdi);

        row = 0;
        t = hig_workarea_create();
        hig_workarea_add_section_title(t, &row, _("Tracker"));
        e = gtk_entry_new();
        gtk_widget_set_size_request(e, 400, -1);
        gtr_paste_clipboard_url_into_entry(e);
        g_object_set_qdata(G_OBJECT(w), URL_ENTRY_KEY, e);
        g_object_set_qdata(G_OBJECT(w), TORRENT_ID_KEY, GINT_TO_POINTER(tr_torrentId(tor)));
        hig_workarea_add_row(t, &row, _("_Announce URL:"), e, NULL);
        gtr_dialog_set_content(GTK_DIALOG(w), t);
        gtk_widget_show_all(w);
    }
}

static void on_tracker_list_remove_button_clicked(GtkButton* button UNUSED, gpointer gdi)
{
    GtkTreeIter iter;
    GtkTreeModel* model;
    struct DetailsImpl* di = gdi;
    GtkTreeView* v = GTK_TREE_VIEW(di->tracker_view);
    GtkTreeSelection* sel = gtk_tree_view_get_selection(v);

    if (gtk_tree_selection_get_selected(sel, &model, &iter))
    {
        int torrent_id;
        int tracker_id;
        tr_variant top;
        tr_variant* args;
        tr_variant* trackers;

        gtk_tree_model_get(model, &iter,
            TRACKER_COL_TRACKER_ID, &tracker_id,
            TRACKER_COL_TORRENT_ID, &torrent_id,
            -1);

        tr_variantInitDict(&top, 2);
        tr_variantDictAddStr(&top, TR_KEY_method, "torrent-set");
        args = tr_variantDictAddDict(&top, TR_KEY_arguments, 2);
        tr_variantDictAddInt(args, TR_KEY_id, torrent_id);
        trackers = tr_variantDictAddList(args, TR_KEY_trackerRemove, 1);
        tr_variantListAddInt(trackers, tracker_id);

        gtr_core_exec(di->core, &top);
        refresh(di);

        tr_variantFree(&top);
    }
}

static GtkWidget* tracker_page_new(struct DetailsImpl* di)
{
    gboolean b;
    GtkCellRenderer* r;
    GtkTreeViewColumn* c;
    GtkTreeSelection* sel;
    GtkWidget* vbox;
    GtkWidget* sw;
    GtkWidget* w;
    GtkWidget* v;
    GtkWidget* hbox;
    int const pad = (GUI_PAD + GUI_PAD_BIG) / 2;

    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), GUI_PAD_BIG);

    di->tracker_store = gtk_list_store_new(TRACKER_N_COLS,
        G_TYPE_INT,
        G_TYPE_STRING,
        G_TYPE_BOOLEAN,
        G_TYPE_INT,
        GDK_TYPE_PIXBUF,
        G_TYPE_BOOLEAN,
        G_TYPE_STRING);

    di->tracker_hash = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free,
        (GDestroyNotify)gtk_tree_row_reference_free);

    di->trackers_filtered = gtk_tree_model_filter_new(GTK_TREE_MODEL(di->tracker_store), NULL);
    gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(di->trackers_filtered), trackerVisibleFunc, di, NULL);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, GUI_PAD_BIG);

    v = di->tracker_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(di->trackers_filtered));
    g_object_unref(di->trackers_filtered);
    gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(v), FALSE);
    g_signal_connect(v, "button-press-event", G_CALLBACK(on_tree_view_button_pressed), NULL);
    g_signal_connect(v, "button-release-event", G_CALLBACK(on_tree_view_button_released), NULL);

    sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(v));
    g_signal_connect(sel, "changed", G_CALLBACK(on_tracker_list_selection_changed), di);

    c = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(c, _("Trackers"));
    gtk_tree_view_append_column(GTK_TREE_VIEW(v), c);

    r = gtk_cell_renderer_pixbuf_new();
    g_object_set(r, "width", 20 + (GUI_PAD_SMALL * 2), "xpad", GUI_PAD_SMALL, "ypad", pad, "yalign", 0.0F, NULL);
    gtk_tree_view_column_pack_start(c, r, FALSE);
    gtk_tree_view_column_add_attribute(c, r, "pixbuf", TRACKER_COL_FAVICON);

    r = gtk_cell_renderer_text_new();
    g_object_set(G_OBJECT(r), "ellipsize", PANGO_ELLIPSIZE_END, "xpad", GUI_PAD_SMALL, "ypad", pad, NULL);
    gtk_tree_view_column_pack_start(c, r, TRUE);
    gtk_tree_view_column_add_attribute(c, r, "markup", TRACKER_COL_TEXT);

    sw = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(sw), v);
    w = gtk_frame_new(NULL);
    gtk_frame_set_shadow_type(GTK_FRAME(w), GTK_SHADOW_IN);
    gtk_container_add(GTK_CONTAINER(w), sw);

    gtk_box_pack_start(GTK_BOX(hbox), w, TRUE, TRUE, 0);

    v = gtk_box_new(GTK_ORIENTATION_VERTICAL, GUI_PAD);

    w = gtk_button_new_with_mnemonic(_("_Add"));
    di->add_tracker_button = w;
    g_signal_connect(w, "clicked", G_CALLBACK(on_tracker_list_add_button_clicked), di);
    gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0);

    w = gtk_button_new_with_mnemonic(_("_Edit"));
    gtk_button_set_image(GTK_BUTTON(w), gtk_image_new_from_icon_name(GTK_STOCK_EDIT, GTK_ICON_SIZE_BUTTON));
    g_signal_connect(w, "clicked", G_CALLBACK(on_edit_trackers), di);
    di->edit_trackers_button = w;
    gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0);

    w = gtk_button_new_with_mnemonic(_("_Remove"));
    di->remove_tracker_button = w;
    g_signal_connect(w, "clicked", G_CALLBACK(on_tracker_list_remove_button_clicked), di);
    gtk_box_pack_start(GTK_BOX(v), w, FALSE, FALSE, 0);

    gtk_box_pack_start(GTK_BOX(hbox), v, FALSE, FALSE, 0);

    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);

    w = gtk_check_button_new_with_mnemonic(_("Show _more details"));
    di->scrape_check = w;
    b = gtr_pref_flag_get(TR_KEY_show_tracker_scrapes);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b);
    g_signal_connect(w, "toggled", G_CALLBACK(onScrapeToggled), di);
    gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0);

    w = gtk_check_button_new_with_mnemonic(_("Show _backup trackers"));
    di->all_check = w;
    b = gtr_pref_flag_get(TR_KEY_show_backup_trackers);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(w), b);
    g_signal_connect(w, "toggled", G_CALLBACK(onBackupToggled), di);
    gtk_box_pack_start(GTK_BOX(vbox), w, FALSE, FALSE, 0);

    return vbox;
}

/****
*****  DIALOG
****/

static void refresh(struct DetailsImpl* di)
{
    int n;
    tr_torrent** torrents = getTorrents(di, &n);

    refreshInfo(di, torrents, n);
    refreshPeers(di, torrents, n);
    refreshTracker(di, torrents, n);
    refreshOptions(di, torrents, n);

    if (n == 0)
    {
        gtk_dialog_response(GTK_DIALOG(di->dialog), GTK_RESPONSE_CLOSE);
    }

    g_free(torrents);
}

static gboolean periodic_refresh(gpointer data)
{
    refresh(data);

    return G_SOURCE_CONTINUE;
}

static void details_free(gpointer gdata)
{
    struct DetailsImpl* data = gdata;
    g_source_remove(data->periodic_refresh_tag);
    g_hash_table_destroy(data->tracker_hash);
    g_hash_table_destroy(data->webseed_hash);
    g_hash_table_destroy(data->peer_hash);
    g_string_free(data->gstr, TRUE);
    g_slist_free(data->ids);
    g_free(data);
}

GtkWidget* gtr_torrent_details_dialog_new(GtkWindow* parent, TrCore* core)
{
    GtkWidget* d;
    GtkWidget* n;
    GtkWidget* v;
    GtkWidget* w;
    GtkWidget* l;
    struct DetailsImpl* di = g_new0(struct DetailsImpl, 1);

    /* one-time setup */
    if (ARG_KEY == 0)
    {
        ARG_KEY = g_quark_from_static_string("tr-arg-key");
        DETAILS_KEY = g_quark_from_static_string("tr-details-data-key");
        TORRENT_ID_KEY = g_quark_from_static_string("tr-torrent-id-key");
        TEXT_BUFFER_KEY = g_quark_from_static_string("tr-text-buffer-key");
        URL_ENTRY_KEY = g_quark_from_static_string("tr-url-entry-key");
    }

    /* create the dialog */
    di->core = core;
    di->gstr = g_string_new(NULL);
    d = gtk_dialog_new_with_buttons(NULL, parent, 0, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL);
    di->dialog = d;
    gtk_window_set_role(GTK_WINDOW(d), "tr-info");
    g_signal_connect_swapped(d, "response", G_CALLBACK(gtk_widget_destroy), d);
    gtk_container_set_border_width(GTK_CONTAINER(d), GUI_PAD);
    g_object_set_qdata_full(G_OBJECT(d), DETAILS_KEY, di, details_free);

    n = gtk_notebook_new();
    gtk_container_set_border_width(GTK_CONTAINER(n), GUI_PAD);

    w = info_page_new(di);
    l = gtk_label_new(_("Information"));
    gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);

    w = peer_page_new(di);
    l = gtk_label_new(_("Peers"));
    gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);

    w = tracker_page_new(di);
    l = gtk_label_new(_("Trackers"));
    gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);

    v = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    di->file_list = gtr_file_list_new(core, 0);
    di->file_label = gtk_label_new(_("File listing not available for combined torrent properties"));
    gtk_box_pack_start(GTK_BOX(v), di->file_list, TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(v), di->file_label, TRUE, TRUE, 0);
    gtk_container_set_border_width(GTK_CONTAINER(v), GUI_PAD_BIG);
    l = gtk_label_new(_("Files"));
    gtk_notebook_append_page(GTK_NOTEBOOK(n), v, l);

    w = options_page_new(di);
    l = gtk_label_new(_("Options"));
    gtk_notebook_append_page(GTK_NOTEBOOK(n), w, l);

    gtr_dialog_set_content(GTK_DIALOG(d), n);
    di->periodic_refresh_tag = gdk_threads_add_timeout_seconds(SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, periodic_refresh, di);
    return d;
}

void gtr_torrent_details_dialog_set_torrents(GtkWidget* w, GSList* ids)
{
    char title[256];
    int const len = g_slist_length(ids);
    struct DetailsImpl* di = g_object_get_qdata(G_OBJECT(w), DETAILS_KEY);

    g_slist_free(di->ids);
    di->ids = g_slist_copy(ids);

    if (len == 1)
    {
        int const id = GPOINTER_TO_INT(ids->data);
        tr_torrent* tor = gtr_core_find_torrent(di->core, id);
        tr_info const* inf = tr_torrentInfo(tor);
        g_snprintf(title, sizeof(title), _("%s Properties"), inf->name);

        gtr_file_list_set_torrent(di->file_list, id);
        gtk_widget_show(di->file_list);
        gtk_widget_hide(di->file_label);
    }
    else
    {
        gtr_file_list_clear(di->file_list);
        gtk_widget_hide(di->file_list);
        gtk_widget_show(di->file_label);
        g_snprintf(title, sizeof(title), _("%'d Torrent Properties"), len);
    }

    gtk_window_set_title(GTK_WINDOW(w), title);

    refresh(di);
}
